import time

from datetime import datetime
from threading import Thread

from main import *
from main.ding_talk import *
from main.do_utils import sql_log, recreate_directory
from main.do_share import share
from main.do_recover import SQLRecover


# 计算时长 :
def run_times(begin_time):
    """
    计算持续时间, 接收一个开始时间, 用当前时间减去开始时间, 得到持续时长
    将持续时长转换为时分秒结构, 精确到秒
    """
    current_time = time.time()
    run_time = current_time - begin_time
    if run_time < 60:
        return f'{int(run_time // 1)} 秒'
    elif 60 ** 2 > run_time >= 60:
        return f'{int(run_time // 60)} 分, {int(run_time % 60)} 秒'
    elif run_time > 60 ** 2:
        return f'{int(run_time // 60 // 60)} 时,  {int(run_time // 60 % 60)} 分, {int(run_time % 60)} 秒'


# 执行备份 :
def db_to_sql(db_conf, folder=None):
    """
    根据 db_conf 中的配置
    链接数据库并进行备份
        1. 查看目标 sql server 的所有数据库
        2. 清空备份目录
        3. 执行备份, 保存到备份目录
    """
    ding_send_begin(db_conf)

    # 使用 mysql client 查看目标 mysql server 的全部 databases :
    os.environ['MYSQL_PWD'] = db_conf.get('password')
    cmd = f"mysql -h{db_conf['host']} -u {db_conf['user']} -P {db_conf['port']} -e \"show databases;\" -B"
    dbs = [line.strip() for line in os.popen(cmd).read().splitlines() if line.strip() not in IGNORE_DBS]
    sql_log.debug(f'mysql数据库: << {db_conf.get('host')} >> 的所有库有: {dbs}')

    # 清空备份目录并重新创建 : ( 全量备份, 仅保留最新一次的备份 )
    backup_folder = folder if folder else BACKUP_FOLDER
    recreate_directory(backup_folder)
    # todo: 这里是只能存储单次备份, 且是先清空, 再备份, 如果备份失败, 则会导致上一次的也没了, 因此最好是有个缓存机制 ...

    # 每次执行备份时, 同步创建恢复文件, 恢复文件中存储的是恢复命令 ( source / cmd ),
    source_recover = SQLRecover(file_name='1_source_recover.txt')
    wincmd_recover = SQLRecover(file_name='1_wincmd_recover.txt')
    # 创建连接信息 :
    source_recover.mysql_connect_info(db_conf)
    wincmd_recover.mysql_connect_info(db_conf)
    # 这里会创建两个恢复文件, 你喜欢哪个就用哪个吧 ~~~
    # todo: 当前的恢复文件还是有点鸡肋的, 虽然实现了复制粘贴直接运行, 但是还是要复制粘贴, 如果是生成 cmd 脚本 (recover.bat), 一键运行就更方便了 ...

    # 开始备份 :
    begin_time = time.time()
    for database in dbs:
        every_begin_time = time.time()
        current_datetime = datetime.now().strftime('%Y%m%d_%H%M%S')
        sql_log.debug(f'Backup << {database} >> begin')
        backup_filename = os.sep.join([backup_folder, f'{database}_{current_datetime}.sql'])
        # 一个 insert 写一个表 :
        backup_command = f"mysqldump --default-character-set=utf8mb4 -h {db_conf['host']} -u {db_conf['user']}  -P {db_conf['port']} {database} > {backup_filename}"
        # 一个 insert 写一个记录 :
        # backup_command = f"mysqldump --skip-extended-insert --default-character-set=utf8 -h {db_conf['host']} -u {db_conf['user']}  -P {db_conf['port']} {database} > {backup_filename}"
        # --skip-extended-insert
        """ # --skip-extended-insert 这个参数用于设置每一个记录对应一个 insert 语句,
        这一点很重要, 因为部分表中存储了一些表情, 这些表情在数据库表中存储是有效的, 但是导出到 sql 文本中就是 ? 来标识 ( 文本并不支持表情 )
        那么如果该字段设置了唯一约束, 就会直接导致写入失败.
        因此, --skip-extended-insert 就非常重要, 
        如果是一个 insert 写入全部数据, 违反唯一约束就会导致写入失败;
        如果是每行数据对应一个 insert 记录, 则违反唯一约束的行单独写入失败;
        
        这里看你怎么取舍吧, 
        你想让违反唯一约束的单独的行失败, 其他成功, 那就加上 --skip-extended-insert 参数
        你想让违反唯一约束的整个表失败, 那就不加 --skip-extended-insert 参数
        
        各有利弊,,,
        
        需要注意:
            不管加不加 --skip-extended-insert 参数, 导出的 sql 文件都是很大的, 
            那么就有一个问题, 如果你想自己编辑该文件, 几乎是不可能, 这么大的文件, 任何编辑器都打不开 ...
            
            比如 yl_share_platform 这个库, 导出来就是 5g 大小, 没有任何编辑器可以正常打开它 ...
            
            那么怎么办呢?
            有一个方式可行, 就是采用 grep 命令来过滤, ( windows 可以用 findstr 来代替, 效率未验证, 你试试吧, 哈哈哈 )
            
            这里给出一个根据报错信息来查找上下文的 grep 命令 : ( 报错是因为 idx_name 是 name 列的索引, 是有唯一约束的 )
            grep -A 20 -B 20 "idx_name" D:\\Useless\\Share\\SQL\\custom_backup\\yl_share_platform_20240711_131727.sql
            使用时需要调整 -A 和 -B 参数的数值, 文件需要用实际备份的文件, 
            
        其他:
            实测 navicat 导出的 sql 脚本中, 表情在文本中正确展示了, 恢复时也不会触发唯一约束而失败;;;
            猜测 mysqldump 在导出数据时应该是可以调整编码以保证表情正确导出为有效记录的 ...
            ---> 妈的, 写了这么多, 我发现实际就是可以使用 utf8mb4 来导出数据库的 ... ... ... 
        """
        os.system(backup_command)
        sql_log.debug(f'Backup created for << {database} >> at << {backup_folder} >> as << {database}_{current_datetime}.sql >>')
        sql_log.debug(f'Backup << {database} >> end, Consumed time << {(time.time() - every_begin_time) // 1} 秒 >>')

        # 数据恢复脚本记录 : ( source / cmd , 均先记录 create 命令, 再记录恢复命令 )
        source_recover.mysql_create(database)
        source_recover.mysql_recover(db_conf, database, current_datetime, backup_folder)
        wincmd_recover.mysql_create(database)
        wincmd_recover.mysql_recover_cmd(db_conf, database, current_datetime, backup_folder)

        # 单个备份成功进行通知 : ( 太多, 就不挨个通知了 ... )
        # ding_send_one(db=database, backup_file=f'{database}_{current_datetime}h.sql')

    consume_time = run_times(begin_time)
    sql_log.info(f'全量备份耗时 : {consume_time}')

    # 循环内部实现了备份, 存储了备份恢复脚本, 循环结束需要生成恢复文件 :
    source_recover.mysql_recover_file(backup_folder)
    wincmd_recover.mysql_recover_file(backup_folder)

    # 钉钉通知 :
    ding_send_all(consume_time, backup_folder)
    ding_send_recover(db_conf)


""" , 此处代码已包含在 win_service.py 中, 配合 windows 服务实现了开机自启动, 因此此处代码注释 ~ 
# 共享 :
share()

# 定义每天凌晨五点执行备份任务 :
schedule.every().day.at('05:00').do(lambda: db_to_sql(DB_INFO_H))

# 启动定时任务 :
while True:
    try:
        schedule.run_pending()
        time.sleep(1)
    except Exception as e:
        sql_log.error(f"Error while running scheduled tasks: {e}")
# """

if __name__ == '__main__':
    """ 目录还是需要讲究的 : 
        此处注意, 钉钉推送的 link 对应是 D:/Useless/Share 的子目录
        钉钉推送脚本中 /SQL/ 是写死的
        因此自定义目录需使用: D:/Useless/Share/SQL/ 再拼接文件名
        比如 : r'D:/Useless/Share/SQL/custom_backup'
    # """

    # 本地运行时, 建议和自动备份的目录区分开 : ( 请仅更改 custom_backup )
    custom_backup = r'D:/Useless/Share/SQL/custom_backup'
    share()
    # backup_thread_debug = Thread(target=db_to_sql, args=(DB_INFO_OWN, custom_backup))
    backup_thread_debug = Thread(target=db_to_sql, args=(DB_INFO_H, custom_backup))
    backup_thread_debug.start()
